﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Collections.ObjectModel;
using Passwords.Models;
using System.Runtime.Serialization;
using System.IO;
using System.IO.Compression;
using Passwords.Exceptions;
using System.Diagnostics;
using Passwords.Utils;
using Passwords.Properties;
using System.Windows.Threading;
using System.Security.Cryptography;
using Passwords.DAL;
using System.Collections;

namespace Passwords.Services
{
    public class SyncContext : INotifyPropertyChanged
    {
        #region fields

        private static SyncContext current;

        private SyncTable syncTable;
        private Dictionary<string, SyncItem> syncedItems;
        private List<SyncItem> syncItems;
        private string syncText;
        private int count;
        private int index;
        private IDataContext dataContext;
        private ObservableCollection<NamedDevice> namedDevices;
        private ObservableCollection<Backup> backups;
        const string backupFolder = "Backups";
        private ServiceState state;
        private SyncMode syncMode;

        #endregion
        #region ctor


        public static SyncContext Current
        {
            get
            {
                return current ?? (current = new SyncContext(null));
            }
        }

        private SyncContext(IDataContext dataContext)
            : base()
        {
            //if (dataContext == null) throw new ArgumentNullException("viewModel");
            this.DataContext = dataContext;

            Update();
        }

        public void Dispose()
        {
            DataContext = null;
            if (Aes != null)
            {
                Aes.Dispose();
                Aes = null;
            }
        }

        ~SyncContext()
        {
            Dispose();
        }

        /// <summary>
        /// Updates the collections in order to use the specified backup folder.
        /// </summary>
        /// 
        public void Update()
        {
            //this.backupFolder = backupFolder;
            if (dataContext != null) dataContext.UpdateProgress();
            //LoadNamedDevices();
            //    LoadBackups();
        }

        #endregion
        #region props

        public Guid DeviceId
        {
            get
            {
                Guid guid = Settings.Default.DeviceId;
                if (guid == Guid.Empty)
                {
                    guid = Guid.NewGuid();
                    try
                    {
                        Settings.Default.DeviceId = guid;
                        Settings.Default.Save();
                    }
                    catch (Exception ex)
                    {
                        if (Debugger.IsAttached) Debugger.Break();
                        Debug.WriteLine(ex.Message);
                    }
                }
                return guid;
            }
        }

        /// <summary>
        /// Gets the sync progressing text.
        /// </summary>
        public string SyncText
        {
            get { return syncText; }
            set
            {
                if (syncText != value)
                {
                    syncText = value;
                    OnPropertyChanged("SyncText");
                }
            }
        }

        /// <summary>
        /// Gets the index of the category or password which is currently sending.
        /// </summary>
        public int Index
        {
            get { return index; }
            set
            {
                if (index != value)
                {
                    index = value;
                    OnPropertyChanged("Index");
                }
            }
        }

        /// <summary>
        /// Gets the number of categories or passwords which are currently sending.
        /// </summary>
        public int Count
        {
            get { return count; }
            set
            {
                if (count != value)
                {
                    count = value;
                    OnPropertyChanged("Count");
                }
            }
        }

        protected AesManaged Aes { get; private set; }

        protected Dispatcher Dispatcher
        {
            get { return App.Current.Dispatcher; }
        }

        public IDataContext DataContext
        {
            get { return dataContext; }
            set
            {
                if (dataContext != value)
                {
                    if (dataContext != null) dataContext.SyncContext = null;
                    dataContext = value;
                    if (dataContext != null) dataContext.SyncContext = this;
                }
            }
        }

        public bool Sync { get { return SyncMode == Passwords.Services.SyncMode.Sync; } }

        public ServiceState State
        {
            get { return state; }
            set
            {
                if (state != value)
                {
                    state = value;
                    UpdateSyncText();
                    OnPropertyChanged("State");
                }
            }
        }

        /// <summary>
        /// Sync mode of the current backup in progress.
        /// </summary>
        public SyncMode SyncMode
        {
            get { return syncMode; }
            set
            {
                if (syncMode != value)
                {
                    syncMode = value;
                    OnPropertyChanged("SyncMode");
                }
            }
        }

        /// <summary>
        /// Current Backup in progress, otherwise null.
        /// </summary>
        public Backup Backup { get; set; }


        public ObservableCollection<NamedDevice> NamedDevices
        {
            get
            {
                if (namedDevices == null)
                {
                    namedDevices = new ObservableCollection<NamedDevice>();
                    LoadNamedDevices();
                }
                return namedDevices;
            }
        }

        /// <summary>
        /// Collection of all available backups, or backups in progress.
        /// </summary>
        public ObservableCollection<Backup> Backups
        {
            get
            {
                if (backups == null)
                {
                    backups = new ObservableCollection<Backup>();
                    LoadBackups();
                }
                return backups;
            }
        }

        #endregion
        #region methods

        private void PrepareCategories()
        {
            if (DataContext != null && Backup != null && Backup.Categories == null)
            {
                DeleteNonExistingCategories();

                Backup.Categories = new ObservableCollection<CategoryContract>(DataContext.Categories);
            }
        }

        private void PreparePasswords()
        {
            if (DataContext != null && Backup != null && Backup.Passwords == null)
            {
                DeleteNonExistingPasswords();
                DeleteNonExistingCategories();

                Backup.Passwords = new ObservableCollection<PasswordContract>(DataContext.Passwords);
            }
        }

        public void Invoke(Action a)
        {
            App.Current.Dispatcher.Invoke(a);
        }

        private void InitNamedDevices(IEnumerable<NamedDevice> devices)
        {
            if (devices != null)
            {
                foreach (NamedDevice nd in devices)
                {
                    namedDevices.Add(nd);
                }
            }
        }

        private void LoadNamedDevices()
        {
            try
            {
                namedDevices.Clear();
                using (var storage = DALUtil.GetStorage())
                {
                    string deviceFile = Path.Combine(backupFolder, "NamedDevices.dev");
                    DataContractSerializer serializer = new DataContractSerializer(typeof(NamedDevices));
                    if (storage.FileExists(deviceFile))
                    {

                        using (Stream stream = storage.OpenFile(deviceFile, FileMode.Open, FileAccess.Read))
                        {
                            using (GZipStream deflate = new GZipStream(stream, CompressionMode.Decompress))
                            {
                                var devices = serializer.ReadObject(deflate) as NamedDevices;
                                if (devices != null) InitNamedDevices(devices.Devices);
                            }
                        }
                    }
                }
            }
            catch (Exception e)
            {
                Debug.WriteLine(e.Message);
                //   throw new LoadNamedDevicesException(e);
            }
        }

        private void SaveNamedDevices()
        {
            try
            {
                string deviceFile = Path.Combine(backupFolder, "NamedDevices.dev");
                DataContractSerializer serializer = new DataContractSerializer(typeof(NamedDevices));
                using (var storage = DALUtil.GetStorage())
                {
                    using (Stream stream = storage.OpenFile(deviceFile, FileMode.Create, FileAccess.Write))
                    {
                        using (GZipStream deflate = new GZipStream(stream, CompressionMode.Compress))
                        {
                            NamedDevices devices = new NamedDevices { Devices = namedDevices.ToArray() };
                            serializer.WriteObject(deflate, devices);
                        }
                    }
                }
            }
            catch (Exception e)
            {
                throw new SaveNamedDevicesException(e);
            }
        }

        private void LoadBackups()
        {
            try
            {
                backups.Clear();

                using (var storage = DALUtil.GetStorage())
                {

                    if (!storage.DirectoryExists(backupFolder))
                    {
                        storage.CreateDirectory(backupFolder);
                    }
                    string pattern = Path.Combine(backupFolder, "*.bak");
                    string[] files = storage.GetFileNames(pattern);

                    DataContractSerializer serializer = new DataContractSerializer(typeof(Backup));
                    List<Backup> list = new List<Backup>(files.Length);
                    foreach (string fileName in files)
                    {
                        try
                        {
                            string file = Path.Combine(backupFolder, fileName);
                            using (Stream fs = storage.OpenFile(file, FileMode.Open, FileAccess.Read))
                            {
                                using (GZipStream deflate = new GZipStream(fs, CompressionMode.Decompress))
                                {
                                    Backup backup = serializer.ReadObject(deflate) as Backup;
                                    if (!backup.IsImported)
                                    {
                                        backup.Name = NameFromDeviceId(backup.DeviceId);
                                    }
                                    list.Add(backup);
                                }
                            }
                        }
                        catch (Exception e)
                        {
                            Trace.WriteLine("Could not read backup");
                            Trace.WriteLine(e.Message);
                        }
                    }
                    BackupComparer comparer = new BackupComparer();
                    list.Sort(comparer);
                    foreach (var item in list)
                    {
                        backups.Add(item);
                    }
                }
            }
            catch (Exception e)
            {
                throw new LoadBackupsException(e);
            }
        }

        public void SaveBackup(Backup backup, BackupState state = BackupState.Uploaded)
        {
            if (backup == null) throw new ArgumentNullException("backup");
            try
            {
                backup.State = state;

                string fileName = Path.Combine(backupFolder, backup.Id.ToString() + ".bak");
                using (var storage = DALUtil.GetStorage())
                {
                    if (storage.FileExists(fileName))
                    {
                        storage.DeleteFile(fileName);
                    }
                    DataContractSerializer serializer = new DataContractSerializer(typeof(Backup));

                    using (Stream stream = storage.OpenFile(fileName, FileMode.Create, FileAccess.Write))
                    {
                        using (GZipStream deflate = new GZipStream(stream, CompressionMode.Compress))
                        {
                            serializer.WriteObject(deflate, backup);
                        }
                    }
                }
            }
            catch (Exception e)
            {
                throw new SaveBackupException(e, backup);
            }
        }

        public void DeleteBackup(Backup backup)
        {
            if (backup == null) throw new ArgumentNullException("backup");

            try
            {
                backup.State = BackupState.Deleted;
                if (Backups.Remove(backup))
                {
                    string fileName = Path.Combine(backupFolder, backup.Id.ToString() + ".bak");
                    using (var storage = DALUtil.GetStorage())
                    {
                        if (storage.FileExists(fileName))
                        {
                            storage.DeleteFile(fileName);
                        }
                    }
                }
            }
            catch (Exception e)
            {
                throw new DeleteBackupException(e, backup);
            }
        }

        public Backup CreateNewBackup(Guid clientId, Guid deviceId)
        {
            Backup backup = new Backup
            {
                Id = clientId,
                State = BackupState.Uploading,
                Name = NameFromDeviceId(deviceId),
                DateCreated = DateTime.Now,
                Categories = new ObservableCollection<CategoryContract>(),
                Passwords = new ObservableCollection<PasswordContract>()
            };
            App.Current.Dispatcher.Invoke((Action)delegate
            {
                Backups.Insert(0, backup);
            });
            //            OnPropertyChanged("Backups");
            return backup;
        }

        private string NameFromDeviceId(Guid deviceId)
        {
            NamedDevice device = NamedDevices.Where(d => d.Guid == deviceId).FirstOrDefault();
            if (device == null)
            {
                string name = string.Empty;
                if (deviceId == this.DeviceId)
                {
                    name = Environment.MachineName;
                }
                else
                {
                    name = "Phone";
                    if (NamedDevices.Count > 0) name += " " + (namedDevices.Count + 1).ToString();
                }
                device = new NamedDevice { Guid = deviceId, Name = name };
                namedDevices.Add(device);
                RenameDevice(deviceId, name);
                SaveNamedDevices();
                OnDeviceAdded(device);
            }
            return device.Name;
        }


        public void RenameDevice(Guid deviceId, string name)
        {
            NamedDevice device = namedDevices.Where(d => d.Guid == deviceId).FirstOrDefault();
            if (device != null) device.Name = name;
            foreach (var b in Backups)
            {
                if (b.DeviceId == deviceId) b.Name = name;
            }
        }

        private void OnDeviceAdded(NamedDevice device)
        {
            var eh = DeviceAdded;
            if (eh != null) eh(device, EventArgs.Empty);
        }

        public Backup FindBackup(Guid clientId)
        {
            Backup backup = Backups.Where(b => b.Id == clientId).FirstOrDefault();
            return backup;
        }

        public int CountPasswords(Guid client)
        {
            if (Sync) PreparePasswords();
            return Backup != null ? Backup.PasswordCount : 0;
        }

        public int CountCategories(Guid client)
        {
            if (Sync) PrepareCategories();
            return Backup != null ? Backup.Categories.Count : 0;
        }

        public void RaiseStateChanged(ServiceState state)
        {
            State = state;
        }

        public PasswordContract GetPassword(int index)
        {
            Backup backup = Backup;
            if (backup != null)
            {
                Count = backup.PasswordCount;
                Index = index;
                UpdateSyncText();
                if (backup.Passwords.Count > index && index >= 0)
                {
                    PasswordContract password = backup.Passwords[index];
                    if (Sync) syncItems.Add(new SyncItem { Id = password.Guid.ToString() });
                    if (Sync) ServiceUtil.EncryptFields(password, Aes);
                    return password;
                }
            }
            return null;
        }

        public CategoryContract GetCategory(int index)
        {
            Backup backup = Backup;
            if (backup != null)
            {
                Count = backup.CategoryCount;
                Index = index;
                UpdateSyncText();
                if (backup.Categories.Count > index && index >= 0)
                {
                    CategoryContract category = backup.Categories[index];
                    if (Sync) syncItems.Add(new SyncItem { Id = category.Id });
                    category.TransferFields = category.Fields.ToArray();
                    return category;
                }
            }
            return null;
        }

        public UpdateMode SendPassword(PasswordContract password)
        {
            if (Sync)
            {
                if (DataContext == null) return UpdateMode.Rejected;

                SyncItem syncItem = GetSyncItem(password.Guid.ToString());
                if (syncItem != null) syncItem.IsAvailable = true;

                PasswordContract src = DataContext.FindPassword(password.Guid);
                if (src == null)
                {

                    bool isSynced = IsIdSynced(password.Guid.ToString(), password.Modified);
                    if (isSynced)
                    {
                        return UpdateMode.Deleted;
                    }
                    else
                    {
                        password.UpdateFields(ServiceUtil.DecryptFields(password, Aes));
                        Invoke(() =>
                        {
                            DataContext.AddPassword(password);
                        });
                        return UpdateMode.Unchanged;
                    }
                }
                else
                {
                    if (src.Modified >= password.Modified)
                    {
                        return UpdateMode.Unchanged;
                    }
                    else
                    {
                        if (Settings.Default.SyncUpdatePassword)
                        {
                            string origName = src.Name;
                            src.CopyFrom(password);
                            Invoke(() =>
                            {
                                src.UpdateFields(ServiceUtil.DecryptFields(password, Aes));
                                DataContext.SavePassword(src, origName);
                            });
                        }
                        return UpdateMode.Modified;
                    }
                }
            }
            else
            {
                Backup backup = Backup;
                if (backup != null)
                {
                    backup.AddPassword(password);
                    return UpdateMode.Unchanged;
                }
            }

            return UpdateMode.Rejected;

        }

        private bool IsIdSynced(string id, DateTime modified)
        {
            SyncItem item;
            if (!syncedItems.TryGetValue(id, out item)) return false;

            return item.SyncDate >= modified;
        }




        public UpdateMode DeletePassword(PasswordContract password)
        {
            Backup backup = Backup;
            if (backup != null)
            {
                backup.DeletePassword(password);
                if (Sync)
                {
                    if (DataContext == null) return UpdateMode.Rejected;

                    PasswordContract pw = DataContext.FindPassword(password.Guid);
                    if (pw != null)
                    {
                        Invoke(() => DataContext.DeletePassword(pw));
                    }
                }
                return UpdateMode.Deleted;
            }
            return UpdateMode.Rejected;

        }

        public UpdateMode SendCategory(CategoryContract category)
        {
            if (Sync)
            {
                if (DataContext == null) return UpdateMode.Rejected;

                SyncItem syncItem = GetSyncItem(category.Id);
                if (syncItem != null) syncItem.IsAvailable = true;
                CategoryContract src = DataContext.FindCategory(category.Id);
                if (src == null)
                {
                    bool isSynced = IsIdSynced(category.Id, category.Modified);
                    if (isSynced)
                    {
                        return UpdateMode.Deleted;
                    }
                    else
                    {
                        Invoke(() => DataContext.AddCategory(category));
                        return UpdateMode.Unchanged;
                    }
                }
                else
                {
                    if (src.Modified >= category.Modified)
                    {
                        return UpdateMode.Unchanged;
                    }
                    else
                    {
                        if (Settings.Default.SyncUpdateCategory)
                        {
                            string originalName = src.Name;
                            Invoke(() =>
                            {
                                category.CopyTo(src);
                                DataContext.SaveCategory(src, originalName);
                            });
                        }
                        return UpdateMode.Modified;
                    }
                }
            }
            else
            {
                Backup backup = Backup;
                if (backup != null)
                {
                    backup.AddCategory(category);
                    return UpdateMode.Unchanged;
                }
            }

            return UpdateMode.Rejected;
        }

        private SyncItem GetSyncItem(string id)
        {
            SyncItem item;
            if (syncedItems.TryGetValue(id, out item)) return item;
            return null;
        }


        public byte[] GetPasswordHash()
        {
            if (Sync)
            {
                if (DataContext == null) return null;
                return DataContext.GetPasswordHash();
            }
            else
            {
                var backup = Backup;
                return backup != null ? backup.PasswordHash : (byte[])null;
            }
        }

        public Backup GetSyncBackup(Guid sessionId, Guid deviceId)
        {
            Backup backup = new Backup { DeviceId = deviceId, Name = "Sync" };
            backup.Id = Guid.NewGuid();
            backup.DateCreated = DateTime.Now;

            return backup;
        }

        public void SetPassword(System.Security.SecureString password)
        {
            if (password == null) return;

            Aes = new AesManaged();
            byte[] iv, key;
            DALUtil.CreateCryptoKey(password, out key, out iv);
            Aes.Key = key;
            Aes.IV = iv;
        }

        private void UpdateSyncText()
        {
            SyncText = GetSyncText();
        }

        private string GetSyncText()
        {
            switch (state)
            {
                default: return string.Empty;
                case ServiceState.Logout: return "Logout...";
                case ServiceState.DeleteCategory: return "Deleting category...";
                case ServiceState.DeletePassword: return "Deleting password...";
                case ServiceState.GetBackups: return "Get backups...";
                case ServiceState.GetCategory: return string.Format("Sending category {0} of {1}", index, count);
                case ServiceState.GetCategoryCount: return "Counting categories...";
                case ServiceState.GetPassword: return string.Format("Sending password {0} of {1}...", index, count);
                case ServiceState.GetPasswordCount: return "Counting passwords...";
                case ServiceState.SendCategory: return "Receving category...";
                case ServiceState.SendPassword: return "Receving password...";
            }
        }

        public void PrepareSync(Guid deviceId)
        {
            if (Sync)
            {
                if (DataContext == null) throw new ArgumentNullException("DataContext");

                syncItems = new List<SyncItem>();
                syncTable = DataContext.GetSyncTable(deviceId);
                if (syncTable == null) syncTable = new SyncTable { DeviceId = deviceId };

                if (syncTable.Items != null) syncedItems = syncTable.Items.ToDictionary(k => k.Id, v => v);
                else syncedItems = new Dictionary<string, SyncItem>();
            }
        }

        public void Logout()
        {
            if (Sync)
            {
                if (syncTable == null) throw new ArgumentNullException("syncTable");
                if (DataContext == null) throw new ArgumentNullException("DataContext");

                // adding 1 minute to the current time since the time on the phone or device and the time on the server might not be synchronized:
                DateTime syncDate = DateTime.Now + TimeSpan.FromMinutes(1.0);
                syncTable.Items = syncItems.ToArray();
                foreach (var item in syncTable.Items) item.SyncDate = syncDate;
                DataContext.SaveSyncTable(syncTable);
            }
        }

        private void DeleteNonExistingPasswords()
        {
            if (syncTable.Items != null && Settings.Default.SyncDeletePassword)
            {
                var nonExistingPasswords = (from item in syncTable.Items
                                            join password in DataContext.Passwords on item.Id equals password.Guid.ToString()
                                            where !item.IsAvailable && password != null && password.Modified <= item.SyncDate
                                            select password).ToArray();


                foreach (PasswordContract password in nonExistingPasswords)
                {
                    Invoke(() => DataContext.DeletePassword(password));
                }
            }
        }

        private void DeleteNonExistingCategories()
        {
            if (syncTable.Items != null && Settings.Default.SyncDeleteCategory)
            {
                var nonExistingCategories = (from item in syncTable.Items
                                             join category in DataContext.Categories on item.Id equals category.Id
                                             where !item.IsAvailable && category != null && category.Modified <= item.SyncDate
                                             select category).ToArray();

                foreach (CategoryContract category in nonExistingCategories)
                {
                    Invoke(() => DataContext.DeleteCategory(category));
                }
            }
        }



        protected void OnPropertyChanged(string propName)
        {
            var eh = PropertyChanged;
            if (eh != null) eh(this, new PropertyChangedEventArgs(propName));
        }

        #endregion
        #region events


        public event PropertyChangedEventHandler PropertyChanged;
        public event EventHandler DeviceAdded;

        #endregion






        public bool Debbuger { get; set; }
    }
}
